#!/usr/bin/env python2.5
# This file is part of Loquacious Etymologist.
# Loquacious Etymologist is copyright (c) Nicholas Pilon 2008.
# The algorithm used by Loquacious Etymologist was designed by John Morrow.
# 
# Loquacious Etymologist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# Loquacious Etymologist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with Loquacious Etymologist.  If not, see <http://www.gnu.org/licenses/>.

import collections
import random
import unicodedata
import codecs

VOWEL = 0
CONSONANT = 1 # Also includes punctuation.

def consecutiveElementsIterator(iteratable):
	"""Iterates through pairs of consecutive elements (in the form of tuples) of elements from iteratable."""
	former = None
	for item in iteratable:
		if former is not None:
			yield (former, item)
		former = item

def groupIterator(iteratable, groupSize):
	"""Iterates through groups of elements from iteratable. Groups are lists of consecutive non-overlapping elements."""
	accumulator = []
	for item in iteratable:
		accumulator.append(item)
		if len(accumulator) == groupSize:
			yield accumulator
			accumulator = []

def randomChoiceFromFrequencyDictionary(dictionary, totalThings):
	"""Chooses a random entry from a dictionary of the format things -> frequency."""
	choice = random.uniform(0, totalThings)
	cumulativeFrequency = 0
	for (thing, frequency) in dictionary.iteritems():
		cumulativeFrequency += frequency
		if choice <= cumulativeFrequency:
			return thing

def randomChoice(possibilityIteratable, frequencyFunction, totalChoices = None, filterFunction = lambda x: True):
	"""Chooses a random entry from an iteratable collection of items, where the frequency of each item can be obtained by applying frequencyFunction to it. The sum of all these frequencies must be totalChoices. (This will be computed if totalChoices is not provided.)
		
		Entries can also be filtered. The filterFunction should accept a single argument and return True or False, appropriately.
	"""
	if totalChoices is None:
		totalChoices = sum(frequencyFunction(possibility) for possibility in possibilityIteratable if filterFunction(possibility))
	choice = random.uniform(0, totalChoices)
	cumulativeFrequency = 0
	for possibility in possibilityIteratable:
		if not filterFunction(possibility):
			continue # Skip this one if we're filtering it out.
		cumulativeFrequency += frequencyFunction(possibility)
		if choice <= cumulativeFrequency:
			return possibility

def classifyCharacter(character, vowels, ignoreDiacriticals):
	"""Classifies the given character as vowel or consonant."""
	if ignoreDiacriticals:
		decomposedCharacter = unicodedata.decomposition(character)
		while decomposedCharacter != '':
			character = unichr(int(decomposedCharacter.split()[0], 16))
			decomposedCharacter = unicodedata.decomposition(character)
	if character in vowels:
		return VOWEL
	else:
		return CONSONANT

class Cluster(object):
	"""A cluster is a group of characters of the same type.
		
		Cluster objects also track information about their role in a word list, which is used to build more words. This is:
		- A set of Clusters that can come after this Cluster.
		- The frequency of this Cluster as the first Cluster in a word.
		- The frequency of this Cluster in the middle of a word.
		- The frequency of this Cluster at the end of a word.
	"""
	def __init__(self, characters, type, succeedingClusters = None, startFrequency = 0, middleFrequency = 0, endFrequency = 0):
		super(Cluster, self).__init__()
		self.characters = characters
		self.type = type
		self.succeedingClusters = succeedingClusters or set() # Clusters that can come after this cluster.
		self.startFrequency = startFrequency # Frequency at the start of words.
		self.middleFrequency = middleFrequency # Frequency in the middle of words.
		self.endFrequency = endFrequency # Frequency at the end of words.
	
	# def __repr__(self):
	# 	return "Cluster(%(characters)r, %(type)r, %(succeedingClusters)r, %(startFrequency)r, %(middleFrequency)r, %(endFrequency)r)" % self.__dict__
	

def clusterize(word, vowels, ignoreDiacriticals):
	"""Splits the given word into clusters of consonants, punctuation, and vowels.
		
		Vowels are defined by the argument, consonants are any other character. Numeric digits should never appear. Optionally, diacriticals can be ignored when 
		
		The clusters are returned as a list of Cluster objects.
	"""
	word.lower()
	clusters = []
	currentCluster = Cluster(word[0], classifyCharacter(word[0], vowels, ignoreDiacriticals))
	for character in word[1:]:
		if classifyCharacter(character, vowels, ignoreDiacriticals) != currentCluster.type:
			clusters.append(currentCluster)
			currentCluster = Cluster(character, classifyCharacter(character, vowels, ignoreDiacriticals))
		else:
			currentCluster.characters += character
	clusters.append(currentCluster)
	
	return clusters

class Language(object):
	"""Provides the global information needed to describe a language and construct words from it."""
	
	def __init__(self):
		super(Language, self).__init__()
		self.numberOfWords = 0
		# ^- Also the count of starting and ending clusters, since each word only has one.
		self.numberOfMiddleClusters = 0
		self.clusterForCharacters = {}
		self.vowelClusterCountFrequency = collections.defaultdict(int)
		self.processedWords = set()
		self.vowels = u'aeiouy'
		self.ignoreDiacriticals = False
	
	def integrateWord(self, word):
		"""Integrates the word list into this language database."""
		if word in self.processedWords:
			return
		self.processedWords.add(word)
		self.numberOfWords += 1
		clusters = clusterize(word, self.vowels, self.ignoreDiacriticals)
		for (index, cluster) in enumerate(clusters):
			if cluster.characters in self.clusterForCharacters:
				clusters[index] = self.clusterForCharacters[cluster.characters]
			else:
				self.clusterForCharacters[cluster.characters] = cluster
		for (preceeding, succeeding) in consecutiveElementsIterator(clusters):
			preceeding.succeedingClusters.add(succeeding)
		clusters[0].startFrequency += 1
		for cluster in clusters[1:-1]:
			cluster.middleFrequency += 1
			self.numberOfMiddleClusters += 1
		clusters[-1].endFrequency += 1
		self.vowelClusterCountFrequency[sum(1 for cluster in clusters if cluster.type == VOWEL)] += 1
	
	def generateWord(language, syllables = None):
		"""Generates a word from the given language. Optionally, the number of syllables (vowel clusters) can be specified."""
		# Choose the number of syllables in the word.
		if syllables is None:
			syllables = randomChoiceFromFrequencyDictionary(language.vowelClusterCountFrequency, language.numberOfWords)
			assert syllables is not None
		# Choose our starting cluster.
		word = u''
		currentCluster = randomChoice( language.clusterForCharacters.values(), lambda x: x.startFrequency, language.numberOfWords )
		word += currentCluster.characters
		if currentCluster.type == VOWEL:
			syllables -= 1
		# Fill in the clusters in the middle.
		while syllables > 1 or currentCluster.type == VOWEL:
			nextCluster = randomChoice( currentCluster.succeedingClusters, lambda x: x.middleFrequency )
			if nextCluster is None:
				# Avoid getting cornered.
				nextCluster = randomChoice( language.clusterForCharacters.values(), (lambda x: x.middleFrequency), filterFunction = (lambda x: x.type != currentCluster.type) )
			word += nextCluster.characters
			if nextCluster.type == VOWEL:
				syllables -= 1
			currentCluster = nextCluster
		# Choose our ending clusters. This means either an ending vowel cluster or a vowel cluster followed by a consonant cluster.
		if syllables == 1:
			# Decide whether to end on a vowel cluster or consonant cluster.
			possibleEndingCluster = randomChoice( language.clusterForCharacters.values(), lambda x: x.endFrequency, language.numberOfWords )
			if possibleEndingCluster.type != VOWEL:
				# Choose a non-ending vowel cluster.
				nextCluster = randomChoice(currentCluster.succeedingClusters, lambda x: x.middleFrequency)
				if nextCluster is None:
					# Avoid getting cornered.
					nextCluster = randomChoice( language.clusterForCharacters.values(), lambda x: x.middleFrequency, filterFunction = (lambda x: x.type != currentCluster.type) )
				word += nextCluster.characters
				currentCluster = nextCluster
			# Choose the ending cluster.
			lastCluster = randomChoice( currentCluster.succeedingClusters, lambda x: x.endFrequency )
			if lastCluster is None:
				# Avoid getting cornered.
				lastCluster = possibleEndingCluster
			word += lastCluster.characters
		return word
	
	def _reprocessWords(self):
		"""docstring for _reprocessWords"""
		oldSet = self.processedWords
		self.processedWords = set()
		for word in oldSet:
			self.integrateWord(word)
	
	def vowels():
		doc = "The vowels property."
		def fget(self):
			return self._vowels
		
		def fset(self, value):
			self._vowels = value
			self._reprocessWords()
		
		def fdel(self):
			del self._vowels
		
		return locals()
	
	vowels = property(**vowels())
	
	def ignoreDiacriticals():
		doc = "The ignoreDiacriticals property."
		def fget(self):
			return self._ignoreDiacriticals
		
		def fset(self, value):
			self._ignoreDiacriticals = value
			self._reprocessWords()
		
		def fdel(self):
			del self._ignoreDiacriticals
		
		return locals()
	ignoreDiacriticals = property(**ignoreDiacriticals())
	
	def dump(self, filename):
		"""Save ourselves to a file."""
		fd = codecs.open(filename, 'w', 'utf8')
		fd.write(u'%d %s %d\n' % (self.numberOfWords, self.vowels, self.ignoreDiacriticals))
		for (vowelClusterCount, vowelClusterFrequency) in self.vowelClusterCountFrequency.iteritems():
			fd.write(u'%d %d ' % (vowelClusterCount, vowelClusterFrequency))
		fd.write(u'\n')
		for word in self.processedWords:
			fd.write(word)
			fd.write(u' ')
		fd.write(u'\n')
		for (characters, cluster) in self.clusterForCharacters.iteritems():
			fd.write(u'CD: %s %d %d %d %d\n' % (characters, cluster.type, cluster.startFrequency, cluster.middleFrequency, cluster.endFrequency))
		for (characters, cluster) in self.clusterForCharacters.iteritems():
			fd.write(u'SD: ')
			fd.write(characters)
			fd.write(u' ')
			for linkedCluster in cluster.succeedingClusters:
				fd.write(linkedCluster.characters)
				fd.write(u' ')
			fd.write(u'\n')
		fd.close()
	
	def load(self, filename):
		"""Load a language from a file, overwriting whatever's in this object already."""
		fd = codecs.open(filename, 'r', 'utf8')
		line = fd.readline()
		lineBits = line.strip().split()
		self.numberOfWords = int(lineBits[0])
		self.vowels = lineBits[1]
		self.ignoreDiacriticals = bool(lineBits[2])
		line = fd.readline()
		self.vowelClusterCountFrequency = collections.defaultdict(int)
		for (count, frequency) in groupIterator(line.strip().split(), 2):
			self.vowelClusterCountFrequency[int(count)] = int(frequency)
		line = fd.readline()
		for word in line.strip().split():
			self.processedWords.add(word)
		for line in fd:
			# Each remaining line is either a cluster description or a successor description.
			lineBits = line.strip().split()
			if lineBits[0] == u'CD:':
				# Cluster description.
				self.clusterForCharacters[lineBits[1]] = Cluster(characters = lineBits[1], type = int(lineBits[2]), startFrequency = int(lineBits[3]), middleFrequency = int(lineBits[4]), endFrequency = int(lineBits[5]))
			elif lineBits[0] == u'SD:':
				# Successor description.
				for successor in lineBits[2:]:
					self.clusterForCharacters[lineBits[1]].succeedingClusters.add(self.clusterForCharacters[successor])
			else:
				# Unknown. Boggle and then ignore.
				pass
		fd.close()
		self.numberOfMiddleClusters = sum(cluster.middleFrequency for cluster in self.clusterForCharacters.itervalues())
	

import sys

if __name__ == '__main__':
	# Test procedure.
	# Usage: wordlist_parser.py <wordlist file>
	language = Language()
	wordlist_file = open(sys.argv[1], 'r')
	for line in wordlist_file:
		for word in line.strip().split():
			language.integrateWord(unicode(word, 'utf8'))
	wordlist_file.close()
	print language.generateWord()